use std::str::FromStr;

use mpl_token_metadata::state::{Metadata, TokenMetadataAccount};
use serde_json::json;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
use solana_sdk::message::Message;
use solana_sdk::pubkey::Pubkey;

use crate::client::create_client;
use crate::solana::formula::RarityFormula;

#[derive(serde::Deserialize)]
struct Env {
    rpc_url: url::Url,
    wallet_pubkey: String,
}

#[derive(serde::Deserialize)]
struct Parsed {
    info: SplToken,
}

#[derive(serde::Deserialize)]
struct SplToken {
    mint: String,
    #[serde(rename(deserialize = "tokenAmount"))]
    token_amount: Amount,
}

#[allow(dead_code)]
#[derive(serde::Deserialize)]
struct Amount {
    amount: String,
    #[serde(rename(deserialize = "uiAmountString"))]
    ui_amount_string: String,
    #[serde(rename(deserialize = "uiAmount"))]
    ui_amount: f64,
    decimals: u8,
}

pub async fn get_metadata(token: &str) -> Result<Metadata, Box<dyn std::error::Error>> {
    let rpc_client = RpcClient::new("https://ssc-dao.genesysgo.net/".to_string());
    let (meta_addr, _) =
        mpl_token_metadata::pda::find_metadata_account(&Pubkey::from_str(token).unwrap());
    let metadata_account = match rpc_client.get_account(&meta_addr).await {
        Ok(data) => data,
        Err(_) => return Ok(Metadata::default()),
    };

    let acct = &mut &metadata_account.data[..];

    let mut metadata = Metadata::safe_deserialize::<Metadata>(acct).unwrap();

    metadata.data.uri = metadata.data.uri.replace('\0', "");

    Ok(metadata)
}

pub async fn fetch_metadata_from_uri(
    uri: String,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
    let mut uri = uri.clone();

    let re = regex::Regex::new(
        r"https://nftstorage\.link/ipfs/(.*)/(.*)|https://(.*)\.ipfs\.nftstorage\.link/(.*)",
    )
    .unwrap();

    if let Some(url) = re.captures(&uri) {
        if url.get(1).is_some() {
            uri = format!("https://{}.ipfs.w3s.link/{}", &url[1], &url[2]);
        } else if url.get(3).is_some() {
            uri = format!("https://{}.ipfs.w3s.link/{}", &url[3], &url[4]);
        }
    }

    let client = create_client(None).unwrap();

    let mut _data = match client.get(uri).send().await {
        Ok(data) => match data.json().await {
            Ok(data) => data,
            Err(_) => json!({}),
        },
        Err(_) => json!({}),
    };

    Ok(json!({ "data": _data }))
}

#[async_recursion::async_recursion]
pub async fn get_rarity(mint: &str, formula: RarityFormula) -> anyhow::Result<serde_json::Value> {
    match formula {
        RarityFormula::CoralCube => {
            let client_ = match create_client(None) {
                Ok(client) => client,
                Err(err) => return Err(anyhow::anyhow!(err)),
            };

            let response = match client_
                .get(format!("https://api.coralcube.io/v1/getItem?mint={}", mint))
                .send()
                .await
            {
                Ok(response) => response,
                Err(err) => return Err(anyhow::anyhow!(err)),
            };

            let coral = match response.json::<serde_json::Value>().await {
                Ok(value) => value["rarity_rank"].to_string(),
                Err(_) => String::from("0"),
            };

            Ok(json!({ "coralcube": coral }))
        }
        RarityFormula::MagicEden => {
            let client_ = match create_client(None) {
                Ok(client) => client,
                Err(err) => return Err(anyhow::anyhow!(err)),
            };

            let response = match client_
                .get(format!(
                    "https://api-mainnet.magiceden.io/rpc/getNFTByMintAddress/{}?useRarity=true",
                    mint
                ))
                .send()
                .await
            {
                Ok(response) => response,
                Err(err) => return Err(anyhow::anyhow!(err)),
            };

            let data = response.json::<serde_json::Value>().await.unwrap();

            if let Some(rarity) = data["results"]["rarity"].as_object() {
                let mut ret_val = json!({});

                for rar in rarity.keys() {
                    let rar_ = rarity.get(rar).unwrap()["rank"].to_string();
                    ret_val[rar.to_string()] = json!(rar_);
                }

                Ok(ret_val)
            } else {
                Ok(json!({}))
            }
        }
        RarityFormula::All => {
            let futures_ = futures::future::join(
                get_rarity(mint, RarityFormula::CoralCube),
                get_rarity(mint, RarityFormula::MagicEden),
            )
            .await;

            let mut coralcube = match futures_.0 {
                Ok(coralcube) => coralcube,
                Err(err) => return Err(anyhow::anyhow!(err)),
            };
            let magiceden = match futures_.1 {
                Ok(magiceden) => magiceden,
                Err(err) => return Err(anyhow::anyhow!(err)),
            };

            for rar in magiceden.as_object().unwrap().keys() {
                coralcube[rar.as_str()] = json!(magiceden[rar.as_str()].as_str().unwrap());
            }

            Ok(coralcube)
        }
    }
}

pub async fn verify_signature(
    public_key: &Pubkey,
    message: String,
    signature: String,
) -> Result<bool, nacl::Error> {
    let signature = bs58::decode(signature).into_vec().unwrap();

    nacl::sign::verify(
        signature.as_slice(),
        message.as_bytes(),
        &public_key.to_bytes(),
    )
}

pub async fn get_nft_wallet(address: String) -> Vec<serde_json::Value> {
    let rpc = RpcClient::new("https://ssc-dao.genesysgo.net/".to_owned());

    let target_pubkey = Pubkey::from_str(&address).unwrap();

    let address_accounts = get_token_accounts(&rpc, &target_pubkey).await.unwrap();

    let accounts: Vec<_> = address_accounts
        .iter()
        .filter_map(|x| {
            if let solana_account_decoder::UiAccountData::Json(d) = &x.account.data {
                Some(d)
            } else {
                None
            }
        })
        .collect();

    let mut metadatas = vec![];

    for mint in accounts
        .iter()
        .filter_map(|x| serde_json::from_value::<Parsed>(x.parsed.clone()).ok())
        .filter(|x| x.info.token_amount.decimals == 0 && x.info.token_amount.ui_amount == 1.0)
        .map(|x| x.info.mint)
    {
        let metadata = get_metadata(&mint).await.unwrap();
        let metadata = fetch_metadata_from_uri(metadata.data.uri).await.unwrap();

        metadatas.push(metadata);
    }

    metadatas
}

pub async fn get_token_accounts(
    rpc: &RpcClient,
    owner: &Pubkey,
) -> Result<Vec<solana_client::rpc_response::RpcKeyedAccount>, Box<dyn std::error::Error>> {
    rpc.get_token_accounts_by_owner(
        owner,
        solana_client::rpc_request::TokenAccountsFilter::ProgramId(spl_token::ID),
    )
    .await
    .map_err(|e| e.into())
}
